package upgradecontroller

import (
	"github.com/pkg/errors"
	"github.com/stackrox/rox/generated/storage"
	"github.com/stackrox/rox/pkg/concurrency"
	"github.com/stackrox/rox/pkg/logging"
	"github.com/stackrox/rox/pkg/sync"
	"github.com/stackrox/rox/pkg/version"
)

var (
	log = logging.LoggerForModule()
)

var (
	errUnknown = errors.New("unknown error")
)

type activeSensorConnectionInfo struct {
	conn          SensorConn
	sensorVersion string
}

type upgradeController struct {
	autoTriggerEnabledFlag *concurrency.Flag

	clusterID string
	errorSig  concurrency.ErrorSignal

	// Since we operate on a single sensor connection, every operation should always run guarded
	// by a mutex, to ensure full sequential consistency (and performance is not a concern here).
	mutex sync.Mutex

	activeSensorConn *activeSensorConnectionInfo

	// The storage is safe for concurrent access, but access should still be protected with a lock
	// to make sure that the stored ClusterUpgradeStatus is in a consistent state.
	// The upgradeController for a cluster "owns" the upgrade status for that cluster.
	// When making an update, it makes sure that, when required, it reads the existing value
	// and preserves fields that have to be preserved.
	storage ClusterStorage

	upgradeStatus        *storage.ClusterUpgradeStatus // owned by the controller, only written through
	upgradeStatusChanged bool

	active *activeUpgradeInfo

	timeouts timeoutProvider
}

func (u *upgradeController) initialize() error {
	cluster, err := u.getClusterOrError()
	if err != nil {
		return err
	}

	upgradeStatus := cluster.GetStatus().GetUpgradeStatus()
	if upgradeStatus == nil {
		upgradeStatus = &storage.ClusterUpgradeStatus{}
	}

	// Reset the upgradability status - we always need an active sensor connection to assess this.
	upgradeStatus.Upgradability = storage.ClusterUpgradeStatus_UNSET
	upgradeStatus.UpgradabilityStatusReason = ""

	u.upgradeStatus = upgradeStatus
	process := upgradeStatus.GetMostRecentProcess()
	u.makeProcessActive(cluster, process)
	observeUpgraderTriggered(u.getSensorVersion(), "central-initialization", u.clusterID, process, u.active != nil)

	if err := u.flushUpgradeStatus(); err != nil {
		return errors.Wrap(err, "persisting upgrade status to DB")
	}

	return nil
}

func (u *upgradeController) do(doFn func() error) (err error) {
	if err = u.errorSig.ErrorWithDefault(errUnknown); err != nil {
		return errors.Wrapf(err, "upgrade controller for cluster %s is in error state", u.clusterID)
	}

	panicked := true
	u.mutex.Lock()
	defer u.mutex.Unlock()

	defer func() {
		var errForMetric string
		if p := recover(); p != nil || panicked {
			ue, ok := p.(unexpectedError)
			if !ok {
				// This panic was not generated by `expectNoError`, so re-"throw" it to ensure we don't swallow
				// something more serious, e.g., nil pointer accesses.
				panic(p)
			}
			errWrap := errors.Wrap(ue.err, "unexpected error during upgrade controller operation")
			u.errorSig.SignalWithError(errWrap)
			errForMetric = errWrap.Error()
		} else if err != nil {
			errForMetric = err.Error()
		}
		var process *storage.ClusterUpgradeStatus_UpgradeProcessStatus
		if u.active != nil {
			process = u.active.status
		}
		// call to getSensorVersion must be with mutex locked
		observeUpgraderError(u.getSensorVersion(), u.clusterID, errForMetric, process)
	}()

	err = doFn()
	u.expectNoError(u.flushUpgradeStatus())
	panicked = false

	return
}

func (u *upgradeController) shouldAutoTriggerUpgrade() bool {
	if !u.autoTriggerEnabledFlag.Get() {
		return false // do not auto-trigger upgrade if setting is disabled
	}
	if u.upgradeStatus.GetUpgradability() != storage.ClusterUpgradeStatus_AUTO_UPGRADE_POSSIBLE {
		return false // only auto-trigger upgrade if upgradability indicates auto upgrade is possible
	}
	if u.upgradeStatus.GetMostRecentProcess().GetTargetVersion() == version.GetMainVersion() {
		// do not auto-trigger upgrade if the most recent upgrade process was for the current central version
		// (we don't distinguish success or failure; if success, the above condition should already catch this, as
		// sensor should then be running the current version).
		return false
	}
	if u.activeSensorConn != nil {
		if err := u.activeSensorConn.conn.CheckAutoUpgradeSupport(); err != nil {
			return false
		}
	}
	return true
}

func (u *upgradeController) getSensorVersion() string {
	if u.activeSensorConn == nil {
		return ""
	}
	return u.activeSensorConn.sensorVersion
}
